iT邦幫忙

2026 iThome 鐵人賽

DAY 27
0
Software Development

GPU 效能優化實戰:30 天從 Kernel 到 Profiling (重賽版)系列 第 27 篇

Day 27:把 SA 留在 GPU 之後,這還是同一條 chain 嗎? ({]重賽版[})

  • 分享至 

  • xImage
  •  

其實已經有點不知道在發什麼了 但既然這邊是廢文集散地
就讓我再多發三天的廢文吧~


這幾天簡單來說,就是 SA(Simulated Annealing,模擬退火)這個 Heuristic 演算法本身 使用在LCN(least crossing number) 這個問體,把它做平行化,然後再把它加到 GPU 上面的方法。

那從演算法的設計,到後續放到 GPU 上面,我都把它整理在這裡了。今天是 Day 27,讓我們繼續看下去吧!


Day 26 結尾已經把帳單寫出來了:

CPU creates one proposal
GPU evaluates one proposal
CPU waits for result
CPU decides acceptance
repeat thousands of times

如果 crossing kernel 只要幾十 microseconds,每一步的 launch、synchronization 與 host-device 控制轉移,確實可能比計算本身還貴。

這件事我們做了。Day 26 的 state machine 整段留在 device:

propose
    -> validate
    -> evaluate
    -> accept
    -> commit
    -> publish best
    -> cooling

timed loop 裡的合約是:

H2D bytes      = 0
D2H bytes      = 0
host decisions = 0

對讀過 Day 20、Day 23、Day 25 的人,這不該是驚喜。中間結果能不寫出去就不要寫;沒變的 layout 不要複製;force iteration 能不回 Python 就不要回。今天只是把同一句話套到 SA 的控制流程。

真正需要盯的是 kernel 裡面這張 mapping:

one CUDA block
    -> one shared current layout

chains_per_group = 32
    -> 32 threads 同時提案
    -> 選一個 winner
    -> 做一次 accept / reject

參數名叫 chains。跑起來卻不是 32 條 Markov chain。

今天要回答的不是「要不要每步回 CPU」。今天要回答:

把 loop 留在 GPU 之後,
我們還在跑 Day 26 那條單 proposal SA 嗎?

每步回 host 是手段,不是今天的問題

Payload 很小。Proposal 只有 node id、old position、new position,objective 也只有 (K, n_K, Phi, C)。

小資料、高頻率,帳單不在 PCIe 頻寬,而在固定成本:

useful payload
    a few integers

fixed cost
    kernel launch
    driver scheduling
    stream ordering
    synchronization
    host wake-up

Resident kernel 把循序 loop 從 host 移進 device:

for (int iter = 0; iter < iterations_in_this_launch; ++iter) {
    generate_proposals();
    evaluate_proposals();
    select_one_candidate();
    accept_or_reject();
    commit_current();
    maybe_publish_best();
    cool_temperature();
}
before
    one host decision per step
    frequent H2D / D2H
    frequent synchronization

resident
    zero host decisions inside timed loop
    zero H2D / D2H inside timed loop
    host only schedules work at batch boundaries

Day 25 已經對 force-directed layout 做過同一招:多輪 iteration 留在一個 kernel,positions 仍從 global memory 讀 tile,省的是 launch 和回 Python。今天不是新發明「loop 不要回 host」。

它也沒有把 step 0 到 step 9999 同時執行。單條 chain 仍然有順序:

current state at step t
    -> proposal t
    -> accept / reject
    -> current state at step t + 1

Device residency 加速 sequential control;它沒有消除 Markov chain 的 dependency。跨 round 仍然一步接一步。變了的是:一步裡面看了幾個 proposals、怎麼決定要不要走。


Device-Resident 不等於 Shared-Memory-Resident

「Resident」很容易讓人誤以為整張 graph 永遠放在 SRAM。Day 25 的小圖路徑確實把一份 positions 留在一個 block 的 shared memory 裡。今天這條路徑不是那個意思。

目前 flash_sa_resident_loop_kernel 的主要 state 放在 device global memory:

current_x / current_y
best_x / best_y
current_edge_crossings
best_edge_crossings
candidate_edge_crossings
current / best objective
RNG state
runtime and telemetry

Kernel 裡明確宣告的 shared memory 只有幾個 block-wide control flags:

__shared__ int selected_proposal;
__shared__ int accepted;
__shared__ int publish_best;

因此:

Day 25 小圖路徑
    一份 positions 住在一個 block 的 shared memory

Day 27 resident SA
    決策與 state 留在 GPU 上,資料仍在 DRAM

Global memory traffic 仍然存在。Coordinates、per-edge counts 與 candidate results 仍由 device memory 讀寫。拿掉的是 PCIe、driver 與 host synchronization,不是所有 DRAM access。


Chains Per Group 從第一次出現就該叫 Proposal Workers

目前 kernel 的 mapping 是:

one CUDA block
    -> one group state

one active thread in that block
    -> one proposal worker

many iterations inside the block
    -> one shared current-state trajectory

每一輪中:

if (tid < chains_per_group) {
    proposal[tid] = make_proposal(current, rng[tid]);
    evaluation[tid] = evaluate(current, proposal[tid]);
}

接著 thread 0 從所有 valid candidates 裡選出 lexicographic objective 最好的那一個:

selected = -1;

for (int p = 0; p < chains_per_group; ++p) {
    if (!evaluation[p].valid) continue;

    if (selected < 0 || evaluation[p].objective < evaluation[selected].objective) {
        selected = p;
    }
}

最後只有 selected candidate 進入一次 acceptance decision,整個 group 最多 commit 一個 move。

因此同一 group 內的 32 個 chains 並不是 32 條各有 current state 的 Markov chains:

Day 26 的一條 chain
    sample one proposal
        -> accept / reject
        -> one transition

今天的一個 group round
    32 workers 讀同一份 current
        -> 產生 32 個 candidates
        -> 選一個 winner
        -> 做一次 transition

比較準確的名稱是 parallel proposal workers。參數仍叫 chains_per_group,是歷史名字,不是搜尋語意。

真正獨立的多條 replicas 需要各自保存 current layout、objective、temperature 與 RNG stream。那是 Day 28 的主題。


先選最好再 Metropolis,已經不是同一條 chain

這是今天最容易看走眼的地方。

Day 26 的單 proposal SA 每一步是:

sample one
    -> legal?
    -> Metropolis against current
    -> maybe commit

Resident group round 是:

sample W
    -> drop invalid
    -> choose the lexicographic best among valid
    -> Metropolis that winner against current
    -> maybe commit once

兩個搜尋的 transition distribution 不一樣。

第一,每輪看到「好 proposal」的機會變高。W 個獨立樣本裡的最佳者,期望上優於只抽一個。高溫時這像同時探了 W 個鄰居;低溫時這像每步都先做一次 greedy filter,再決定要不要走。

第二,沒被選上的 valid proposal 從來沒有機會跟 current 做 Metropolis。Day 26 裡它們各自會走一次 accept / reject;今天它們在 winner scan 就被丟了。

第三,比較速度與解品質時,不能把 W 個 candidate evaluations 直接當成 W 個 sequential SA transitions:

candidate evaluations != SA transitions
SA transitions         != best updates
best updates           != K improvements

W = 32、跑 10,000 rounds,是 320,000 次 evaluation,但最多 10,000 次 chain transition。把前者除以後者,會得到一個看起來很勤快的搜尋,實際上 current 只走了 10,000 步,而且每步的提案分布已經被 winner filter 改過。

Thread 0 的 winner scan 本身也是 serial O(W)。當 proposal evaluation 很昂貴時,這段成本很小;若後續 spatial evaluator 已經把評分大幅壓低,它可能成為下一個需要改成 block reduction 的區段。最佳化後必須重新 profile,不能假設原本的小成本會永遠很小。


GPU 上到底有哪些 State?

Day 26 的 current、best,搬到 GPU 後還需要一份 candidate work area。因為現在同一輪有 W 個 workers 同時寫自己的 scratch,不能再靠 Python 那份 list(current_positions) 的複製品。

State 內容 何時修改
Current coordinates、edge counts、objective 唯一的 winner 被接受後
Best coordinates、edge counts、objective current 嚴格優於 best 時
Candidate 每個 worker 的 proposal、objective、edge counts 每輪覆寫
Control temperature、iteration、plateau age 每輪結束,整個 group 共用一份
RNG 每個 worker 的亂數狀態 產生 proposal 與 acceptance draw
Telemetry attempts、valid、accepted、branches、best updates 對應事件發生時

原專案的 workspace 直接預先配置:

current_x, current_y
best_x, best_y
scratch_x, scratch_y

current_edge_crossings
best_edge_crossings
scratch_edge_crossings

current_objective
best_objective
scratch_objective

另外還有每個 candidate worker 的 proposal、evaluation result 與 edge-count buffer。

這些 allocations 在 timed loop 之前完成。Resident tests 明確要求:

timed_cuda_malloc_count = 0
timed_cuda_free_count   = 0

因為 cudaMalloc 不只是取得記憶體,也可能帶來昂貴的 runtime coordination。把 allocation 搬出 hot loop,通常比替一個很小的 device function 手動展開幾行更有價值。

注意 temperature 是 group 共用的。32 個 workers 沒有 32 個溫度。這又是「不是 32 條 chain」的一個具體後果:它們甚至不能各自冷卻。


一輪實際 commit 了什麼

不必把 Day 26 的 SA 再講一次。這裡只看同一輪裡,哪些動作是 W 份平行、哪些動作仍然只有一次。

平行的是提案與評分

每個 worker 有自己的 RNG:

rng[group, worker]

固定模式會選 node,再依目前溫度決定 local radius,寫一份 proposal。Proposal 保存 from_x、from_y。若 current 已經不在那個 old position,這份 proposal 對錯版本的 state,直接標成 invalid。

Day 26 會複製整張 candidate layout。GPU 改用 virtual override,這是 Day 23 同一招:

candidate_x(node) =
    node == moved_node ? proposal.to_x : current_x[node];
full candidate coordinate copy
    O(N) writes per proposal

virtual override
    O(1) proposal state

每個 proposal 先過 V1~V4。非法的不進 winner scan:溫度只能接受較差的合法解,不能接受 geometry violation。

活下來的 worker 才算 candidate edge counts 與 (K, n_K, Phi, C)。Full-square 從頭算全部 pair;incremental 從 current_edge_crossings 出發,只處理 moved node 的 incident edges。若 sum(count[e]) 是奇數,incremental closing state 已經壞了,evaluator 要求 fallback,不能帶著損壞的 counts 繼續跑。

到這裡為止,平行度來自多個 proposals,不是讓整個 block 合作評估一個 proposal。

串行的是選人、接受、發布

所有 workers 完成 evaluation 後:

__syncthreads();

Thread 0 選 winner,再套用 Day 26 的 layered acceptance。接受後,thread 0 先更新 moved node 與 objective,整個 block 再合作複製 selected candidate 的 per-edge counts:

current_x[node] = proposal.to_x;
current_y[node] = proposal.to_y;
current_objective = selected.objective;

for (int edge = tid; edge < E; edge += blockDim.x) {
    current_edge_crossings[edge] = selected_edge_crossings[edge];
}

Rejected 完全不寫 current。這就是 transactional scratch:先在 candidate buffer 形成完整、一致的候選狀態,接受後才發布成 current。

接受也不等於更新 best:

publish_best = accepted && current_objective < best_objective;

只有這時才複製 current → best。最後 thread 0 更新 iteration、plateau age 與 temperature。整個 group 共用這一份控制狀態。

所以一輪結束時,current 最多走一步。W 只改變「這一步看過哪些鄰居」,不改變「這一步能走幾次」。


Barrier 為什麼比程式碼看起來還多?

一輪裡的重要同步點是:

all proposals evaluated
        |
     barrier
        v
thread 0 selects and decides acceptance
        |
     barrier
        v
all threads know accepted and selected index
        |
copy selected counts to current
        |
     barrier
        v
thread 0 decides publish_best
        |
     barrier
        v
copy current state to best if needed
        |
     barrier
        v
update temperature and start next iteration

如果缺少 selected-count copy 後的 barrier,下一輪可能看到:

new coordinates
new objective
partly old edge counts

這份 state 表面上每個欄位都存在,彼此卻描述不同的 layout。Incremental evaluator 會從錯誤 cache 繼續更新,問題可能過很多輪才被 sum(count[e]) 奇偶性或 exact validator 發現。

Winner-then-Metropolis 已經改了搜尋。Barrier 則保證:改過之後的那一份 current,仍然是同一世代的 coordinates、counts 與 objective。Synchronization 在這裡不是單純的效能損失,它界定 commit transaction 的可見性。


Reject 很便宜嗎?W 個工人也不便宜

Reject 確實避免了兩份發布成本:

no current edge-count copy
no best coordinate / count copy

但每個 worker 在被丢掉之前已經做了:

geometry validation
crossing evaluation
candidate objective construction
candidate edge-count writes

若每個 group 有 W 個 workers,每個 worker 保存 E 個 candidate counts,單輪 candidate scratch 至少是:

candidate_count_bytes = W * E * 4

例如:

W = 256
E = 100,000

bytes = 256 * 100,000 * 4
      = 102,400,000 bytes
      = about 97.66 MiB per group

這還沒算 coordinates、current/best counts、objectives、spatial index 與其他 work buffers。

所以「同時產生更多 proposals」會增加平行度,也會線性增加 candidate state。它同時在兩件事上收費:每輪多做的 evaluation,以及那份永遠用不到的 loser scratch。當 evaluator 已經很快時,global-memory footprint 和寫入 candidate counts 可能成為新的瓶頸。

進一步的方向是只保存 incident-edge deltas,接受時再套用到 current counts;代價是 commit 與 rollback contract 會更複雜,而且必須保證沒有漏掉 static edge 被 incident edge 新增或移除的 crossing contribution。


Proposals Per Launch 不是 Concurrent Proposal 數

這裡有兩個很像、但意義不同的參數:

chains_per_group
    同一輪同時評估多少 proposals
    也就是 proposal worker 數
    它改變搜尋語意

proposals_per_launch
    一次 kernel launch 在同一 current trajectory 上執行多少輪
    它只改變控制粒度
    它不改變每一輪仍只 commit 一次

假設:

iterations = 10,000
proposals_per_launch = 4

Resident loop kernel launch 數是:

ceil(10,000 / 4) = 2,500 launches

如果每一步至少需要一個 host-controlled evaluation launch,單看 resident loop launch 可得到:

10,000 / 2,500 = 4x fewer loop launches

這仍不是 4 倍 runtime speedup,更不是 4 倍搜尋進度。每個 batch 後還可能有 group-sync kernel,setup 與 final hash 也有額外 launches,而真正的 evaluator 工作量沒有因 batching 消失。每一輪仍然最多走一步。

把 proposals_per_launch 拉大可以攤平 launch overhead,但會降低控制粒度:

larger batch
    fewer launches
    longer interval before host regains control
    slower response to time limit or external policy changes
    group exchange cannot occur inside the batch boundary

若 sync_interval 比 batch 更早到,host driver 會縮短這次 launch,確保 group synchronization 發生在指定 iteration。Day 28 的 replica exchange 就卡在這個邊界上:exchange 不能發生在 batch 內部,因為 host 當時看不見任何 replica state。


用算術模型看 Evaluation 與 Transition

Day 27 加了一個不需要 GPU 的模型:

python case_4/examples/sa_residency_model.py

預設參數是:

N = 150
E = 300
groups = 1
workers per group = 32
iterations = 10,000
proposals per launch = 4

輸出:

candidate evaluations:             320,000
minimum host evaluation launches:   10,000
resident loop launches:              2,500
resident launch reduction:       4.00x

coordinate states / group:       3.52 KiB
edge-count states / group:       3.52 KiB
candidate edge counts / group:   37.50 KiB
modeled lower bound / group:     44.53 KiB

請先看第一行和「10,000 iterations」的關係:

320,000 evaluations
    / 10,000 rounds
    = 32 workers 同時看同一份 current

最多 10,000 次 transition
    因為每輪只 commit 一次

Memory lower bound 使用:

three coordinate states
    current + best + scratch
    3 * 2 * N * 4 bytes

three edge-count states
    current + best + scratch
    3 * E * 4 bytes

per-worker candidate counts
    W * E * 4 bytes

它沒有包含 allocator metadata、objectives、RNG、telemetry、spatial grid、validator 與其他 candidate buffers,因此只能叫做 lower bound。

同樣地,launch reduction 是控制流算術,不是 GPU benchmark,也不是搜尋進度。


把 Boundary 成本拿掉後,Evaluator 會露出來

在 host-driven path 中,profiler 可能顯示大量:

short kernels
cudaMemcpy
cudaStreamSynchronize
CPU gaps between launches

Resident 化之後,這些固定成本下降,真正的計算才會成為主角。也是在這個時候,才看得出今天這條 chain 的平行度有多窄。

目前 resident evaluator 的工作 mapping 是:

one worker thread
    -> validate one proposal
    -> loop nodes and edges
    -> build one candidate objective

如果 chains_per_group = 32,256-thread block 中只有前 32 個 threads 進行 proposal evaluation,剛好是一個 warp。其餘 threads 要等到 cooperative copy 或 barrier 階段才可能工作。

GPU 並沒有在「加速同一條 Day 26 chain 的單步計算」。它把多餘的 threads 拿去同時產更多 candidates,然後再丟掉大多數。

Full-square evaluator 的單輪幾何工作量接近:

W * E * E crossing predicates

從 Day 23 的演算法目標來看,incremental crossing predicates 應該接近:

W * degree(moved_node) * E

但目前 resident function 仍用兩層 for edge / for other 掃過 E * E 個 pair slots,再用 incident 判斷跳過不受影響的 pair:

pair-loop visits
    W * E * E

expensive intersection predicates
    approximately W * degree(moved_node) * E

所以它確實減少 orientation 與 segment-intersection 計算,卻沒有消除所有平方級 loop、branch 與 edge-endpoint reads。真正的 spatial candidate list 應該連不可能相交的 partner indices 都不要走訪。

當 launch 與 host round-trip 不再遮住它,Day 23 的增量化才會直接決定吞吐量。瓶頸會移動:

remove boundary overhead
    -> expose evaluator cost

remove unnecessary pair work
    -> expose memory traffic

compress candidate state
    -> expose selection and synchronization

使用 persistent kernel 不會讓瓶頸永遠消失。它只是讓你終於看到:這顆 kernel 大部分時間在為不會被 commit 的 proposals 工作。


一個值得警惕的 Backend 標籤問題

PreProposalEvaluatorMode 列出:

FULL_SQUARE_GPU
INCREMENTAL_GPU
INCREMENTAL_C1_DENSE
INCREMENTAL_C2_CLOSING_STATE
INCREMENTAL_C3_SPATIAL

但目前快照中的 flash_resident_evaluate(...) 只把模式分成兩類:

full_square = mode == FULL_SQUARE_GPU;

if full_square:
    recompute all pairs
else:
    use the same incident-edge incremental branch

也就是這個 resident function 本身沒有針對 C1、C2、C3 選擇不同的 specialized implementation。其他 replay 或 qualification paths 有各自的實作,不代表 resident hot loop 自動使用相同的 spatial candidate list。

因此 benchmark 不能只根據 CLI 顯示 INCREMENTAL_C3_SPATIAL,就把效能歸因給 C3。應該用 profiler、kernel symbol、工作量 counters,或刻意設計能區分 C1/C3 掃描量的測試,確認實際執行路徑。

這個例子非常典型:

configuration name
    does not prove
executed optimization

它和 chains_per_group 是同一類陷阱:標籤看起來像你想要的演算法,跑起來是另一個。


Telemetry 的 Rejected 也要小心解讀

每輪 W 個 workers 產生 candidates,最多一個會被 commit。Kernel 的 counter 使用:

rejected_count += W - (accepted ? 1 : 0)

因此 rejected 包含:

invalid proposals
valid but not selected proposals
selected proposal rejected by Metropolis

若 W = 256,即使每輪 winner 都被接受:

accepted = 1
rejected = 255

表面 acceptance rate 只有:

1 / 256

這不能解讀成溫度太低或 Metropolis 幾乎不接受。大部分「rejected」其實連 acceptance branch 都沒有進入。它們是 winner-then-Metropolis 的直接後果,不是 Day 26 那條 chain 的拒答率。

所以還要同時看:

candidate attempts
geometry-valid candidates
selected candidates
Metropolis accept / reject branches
committed moves
best updates

沒有分母定義的 acceptance rate,很容易比較錯誤。尤其是拿它去跟 Day 26 的單 proposal telemetry 對打:兩邊的 rejected 根本不是同一個事件。


Correctness 怎麼證明 Resident Loop 沒有改壞合約?

這裡必須講清楚測了什麼、沒測什麼。

Residency tests 證明的是 state、residency 與 deterministic contract。它們不證明「這還是 Day 26 那條 chain」——因為它本來就不是。Winner-then-Metropolis 是刻意改過的搜尋。測試要守住的是:改過之後,current / best 仍然合法、可重播、而且 timed loop 真的沒有跑回 host。

第一,group 與 worker width:

groups = 1, 2, 4
workers per group = 1, 2, 32, 256

每組的 best objective 必須不差於 current objective:

best <= current

而且 published state 必須 exact_valid = 1。

第二,固定 seed 的 replay 必須 deterministic:

same config + same seed
    -> same current state hash
    -> same best state hash
    -> same objectives

測試涵蓋 full-square 與所有 incremental mode labels。

第三,temperature 必須在 device 更新。10 iterations 後:

actual temperature
    approximately equals
0.1 * 0.999 ** 10

第四,timed loop 的 residency counters 必須是:

cudaMalloc count = 0
cudaFree count   = 0
H2D bytes        = 0
D2H bytes        = 0
host decisions   = 0

第五,非法 worker width 在 launch 前拒絕:

chains_per_group > 256
    -> configuration error

這些測試不自動證明 C3 比 C1 快,不提供端到端 speedup,也不證明 W = 32 的搜尋質量等於 32 條 Day 26 chain。W = 1 那一組才最接近昨天的單 proposal 語意;其餘寬度是另一個演算法。


效能該怎麼量?

目前快照保留 residency runner 與測試,沒有保留能直接引用的同硬體、同 commit、host-driven 對 resident 的正式 timing table。因此這篇不填一個猜測的 speedup,也不用「少了每步回 CPU」去暗示已經很快。

需要分三層測量:

control-plane evidence
    H2D / D2H bytes
    host decision count
    kernel launch count
    synchronization count

kernel evidence
    duration per resident launch
    crossing predicates per second
    global load/store throughput
    branch divergence
    eligible warps and achieved occupancy

search evidence
    proposals per second
    candidate evaluations per second
    committed moves per second
    time to target K
    final validated K at fixed wall-clock budget

其中最重要的單位區別仍然是:

candidate evaluations != SA transitions

Resident path 可能讓 candidate evaluations / second 大幅上升,卻因 winner-selection policy,沒有同比改善 time-to-target-K。那不是量錯,那是今天改過的 chain 在說話。


這個案例介紹了 GPU 最佳化的哪一部分?

Day 25 的 Layout 透過 shared-memory tiling 減少重複讀取,也把多輪 force iteration 留在 device。Day 27 看起來像在處理更上層的系統成本:

keep control and state on device
    -> reduce launch boundaries
    -> remove per-step H2D / D2H
    -> remove host decisions

對這個系列,那句話已經不夠當主軸。更值錢的是:把控制留在 GPU 之後,平行提案會順便改掉搜尋。

具體技術包括:

  1. 預先配置 current、best、candidate 與 validator workspace。
  2. 用 virtual coordinate override 避免每個 proposal 複製整張 layout。
  3. 同時產生多個 proposals,再選出一個 candidate。
  4. 把 validation、objective、acceptance、commit 與 cooling 融進同一 kernel。
  5. 只在 accepted 時發布 current counts。
  6. 只在 strict improvement 時複製 best state。
  7. 保存 per-worker RNG state,讓 device replay 可重現。
  8. 用 barriers 保證 coordinates、counts 與 objective 是同一個 commit generation。
  9. 用 residency counters 證明 timed loop 沒有 allocation、host decision 或 H2D/D2H。

也有幾個反直覺結果:

resident does not mean shared-memory resident

32 chains_per_group is not 32 Markov chains

sample W then Metropolis one
    is not W sequential SA steps

more workers means more candidate memory
    and more evaluations that will never commit

larger proposals_per_launch reduces launches
    but also reduces control responsiveness

configuration name
    does not prove specialized code executed

rejected telemetry is not Metropolis rejection rate

Day 28 會把一個 shared-current group 擴展成真正的多 replica 搜尋。每個 replica 有自己的 layout 與 temperature,GPU 可以讓它們同時探索;難題則變成 replicas 何時交換、交換的是 state 還是 temperature label,以及 acceptance probability 要如何保持正確。

那才是「同時跑很多條 SA」。今天這 32 個 workers,還只是同一條 current 上的平行提案。

程式對照


上一篇
Day 26:明明找到更好的 Layout,為什麼 SA 還要故意走回頭路?({]重賽版[})
下一篇
Day 28:Parallel SA,不是把同一條搜尋複製 512 次 }}重賽版{{_orz-)
系列文
GPU 效能優化實戰:30 天從 Kernel 到 Profiling (重賽版) 共 30 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言